package lv.solcraft.replication.server.bdb;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import lv.solcraft.replication.server.boot.ServerConfig;
import lv.solcraft.replication.server.dao.DataAccessException;
import lv.solcraft.replication.server.dao.OperDataAccess;
import lv.solcraft.replication.server.dao.OperSiteDataAccessFactory;
import lv.solcraft.replication.server.model.OperState;

import org.springframework.util.Assert;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.StoreConfig;

public class Database {

	private static final String OPERS_STORE = "opers";
	private final boolean transactional = true;
	private Environment environment;
	private EntityStore operStore;
	private final Map<String, OperSiteStore> operSiteStores = new ConcurrentHashMap<String, OperSiteStore>();
	private ServerConfig serverConfig;

	private static final ThreadLocal<Transaction> threadTx = new ThreadLocal<Transaction>();

	public void setServerConfig(ServerConfig serverConfig) {
		this.serverConfig = serverConfig;
	}

	public void start() throws Exception {
		EnvironmentConfig envConfig = new EnvironmentConfig();
		envConfig.setAllowCreate(true);
		envConfig.setTransactional(transactional);
		envConfig.setConfigParam("je.lock.nLockTables", "3");
		envConfig.setLockTimeout(10 * 1000000); // 10 seconds
		envConfig.setTxnTimeout(2 * 60 * 1000000); // 2 minutes
		envConfig.setTxnWriteNoSync(true);
		environment = new Environment(getDbPath(), envConfig);
		operStore = createEntityStore(OPERS_STORE);
		readExistingOperSiteStores();
		addOperSiteStores();
	}

	private void addOperSiteStores() {
		for (long siteId : serverConfig.getSitesList()) {
			for (OperState state : OperState.values()) {
				createOperSiteEntityStore(siteId, state);
			}
		}
	}

	protected File getDbPath() {
		File file = new File(serverConfig.getDbPath());
		if (!file.exists())
			file.mkdir();
		Assert.isTrue(file.exists(), "DB directory " + file.getAbsolutePath()
				+ " does not exist");
		return file;
	}

	public Environment getEnvironment() {
		return environment;
	}

	public EntityStore getOperStore() {
		return operStore;
	}

	private OperSiteStore createOperSiteEntityStore(Long siteId,
			OperState operState) {
		String storeName = getOperSiteStoreName(siteId, operState);
		OperSiteStore store = operSiteStores.get(storeName);
		if (store == null) {
			store = new OperSiteStore(createEntityStore(storeName), operState,
					siteId);
			operSiteStores.put(storeName, store);
		}
		return store;
	}

	private EntityStore createEntityStore(String storeName) {
		StoreConfig storeConfig = new StoreConfig();
		storeConfig.setAllowCreate(true);
		storeConfig.setTransactional(transactional);
		try {
			return new EntityStore(environment, storeName, storeConfig);
		} catch (DatabaseException e) {
			throw new DataAccessException(
					"Could not get oper site data store: " + storeName, e);
		}
	}

	private void readExistingOperSiteStores() {
		try {
			for (Object nameObj : environment.getDatabaseNames()) {
				String name = nameObj.toString();
				if (!name.startsWith("persist#opersites"))
					continue;
				// db name is similart to
				// 'persist#opersites:1:NEW#com.sleepycat.per...'
				String[] segments = name.split("#");
				// second segment should containt info we are interested
				String storeName = segments[1];
				segments = storeName.split(":");
				OperState operState = OperState.valueOf(segments[2]);
				long siteId = Long.valueOf(segments[1]);
				createOperSiteEntityStore(siteId, operState);
			}
		} catch (DatabaseException e) {
			throw new DataAccessException(
					"Could not return all oper site stores", e);
		}
	}

	public OperSiteStore getOperSiteStore(Long siteId, OperState operState) {
		OperSiteStore store = operSiteStores.get(this.getOperSiteStoreName(
				siteId, operState));
		Assert.notNull(store, "Oper site store not found, siteId " + siteId
				+ ", operState " + operState);
		return store;
	}

	public static class OperSiteStore implements Comparable<OperSiteStore> {
		private final EntityStore entityStore;
		private final OperState operState;
		private final long siteId;

		public OperSiteStore(EntityStore entityStore, OperState operState,
				long siteId) {
			super();
			this.entityStore = entityStore;
			this.operState = operState;
			this.siteId = siteId;
		}

		public EntityStore getEntityStore() {
			return entityStore;
		}

		public OperState getOperState() {
			return operState;
		}

		public long getSiteId() {
			return siteId;
		}

		public int compareTo(OperSiteStore o) {
			int result = new Long(siteId).compareTo(o.siteId);
			result = result == 0 ? operState.compareTo(o.operState) : result;
			return result;
		}

	}

	protected String getOperSiteStoreName(Long siteId, OperState operState) {
		return "opersites:" + siteId + ":" + operState;
	}

	public void stop() throws Exception {
		for (OperSiteStore store : operSiteStores.values()) {
			store.getEntityStore().close();
		}
		if (operStore != null)
			operStore.close();
		if (environment != null)
			environment.close();
	}

	public void beginTx() {
		try {
			Assert.isTrue(!isActiveTx(), "Tx already started");
			Transaction tx = environment.beginTransaction(null, null);
			threadTx.set(tx);
		} catch (DatabaseException e) {
			throw new DataAccessException(
					"BDB error while starting transaction", e);
		}
	}

	public void commitTx() {
		try {
			Assert.isTrue(isActiveTx(), "Tx not started, can not commit");
			activeTx().commit();
			threadTx.remove();
		} catch (DatabaseException e) {
			throw new DataAccessException(
					"BDB error while commiting transaction", e);
		}
	}

	public void rollbackTx() {
		try {
			Assert.isTrue(isActiveTx(), "Tx not started, can not rollback");
			activeTx().abort();
			threadTx.remove();
		} catch (DatabaseException e) {
			throw new DataAccessException(
					"BDB error while rollback transaction", e);
		}
	}

	public Transaction activeTx() {
		return threadTx.get();
	}

	public boolean isActiveTx() {
		return activeTx() != null;
	}

	public OperDataAccess getOperDataAccess() {
		return new OperDataAccess(this.operStore, this);
	}

	public OperSiteDataAccessFactory getOperSiteDataAccessFactory() {
		return new OperSiteDataAccessFactory(this);
	}

	public Map<String, OperSiteStore> getOperSiteStoresMap() {
		return Collections.unmodifiableMap(operSiteStores);
	}

	public List<OperSiteStore> getOperSiteStores() {
		List<OperSiteStore> operStoresList = new ArrayList<OperSiteStore>(
				operSiteStores.values());
		Collections.sort(operStoresList);
		return Collections.unmodifiableList(operStoresList);
	}

	public void setActiveTx(Transaction tx) {
		threadTx.set(tx);
	}
}
